// gfx.c++
// SDL graphics functions file

#include <iostream>
#include <SDL/SDL.h>
#include <assert.h>
#include <stdlib.h>

#include "highground.h"
#include "gfx.h"


#ifdef WINDOWS
	#include <windows.h>
	#include <io.h>
	#include <FCNTL.H>
	static void activateConsole();
#endif /* WINDOWS */

SDL_Surface *screen = NULL;
SDL_Surface *screenFilter = NULL;
SDL_Surface* tileFilter = NULL;
unsigned char screenFilterAlpha;
extern unsigned int xRes;
extern unsigned int yRes;
extern unsigned int bpp;
extern unsigned int windowX;
extern unsigned int windowY;
extern unsigned int windowW;
extern unsigned int windowH;

// This initialises SDL, and sets up the video mode and window.
// On Windows, the console has to be set up.
void initSDL(const unsigned int xRes, const unsigned int yRes,
             const unsigned int bpp) {
	if( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
		std::cout << "Unable to initialise SDL video: "
                  << SDL_GetError() << std::endl;
		exit(1);
	}
	atexit(SDL_Quit);
	screen = SDL_SetVideoMode(xRes, yRes, bpp, SDL_SWSURFACE);
	if ( screen == NULL ) {
		std::cout << "Unable to set " << xRes << "x" << yRes 
                  << " video mode: " << SDL_GetError() << std::endl;
		exit (1);
	}
	if (SDL_GetVideoSurface()->flags & SDL_HWSURFACE) {
			std::cout << "Using hardware surfaces..." << std::endl;
	} else {
			std::cout << "Using software surfaces..." << std::endl;
	}
	
	#ifdef WINDOWS
		// redirecting the standard input/output to the console 
		// is required with windows.
		activateConsole(); 
	#endif // WINDOWS

	if (screen->format->BytesPerPixel != (bpp / 8)) {
		std::cout << "Unable to set the correct bpp (" << bpp << " bits)."
		          << std::endl << "You need to adjust your display settings."
		          << std::endl;
  	}
	std::cout << "SDL video initialised. Screen set at: " << xRes << "x" << yRes
	          << ". Colour depth: " << bpp << " bits." << std::endl;
	SDL_WM_SetCaption("High Ground", "High Ground");
	
}

// Locks an SDL surface so that it may be directly edited.
// This is necessary for hardware surfaces.
void sLock(SDL_Surface* surface) {
	if (SDL_MUSTLOCK(surface)) {
		if (SDL_LockSurface(surface) < 0) {
			std::cout << "WARNING: Could not lock surface." << std::endl;
		}
	}
}

// Unlocks and SDL surface.
void sUlock(SDL_Surface* surface) {
	if (SDL_MUSTLOCK(surface)) {
		SDL_UnlockSurface(surface);
	}
}

// Draws part of an SDL surface to the screen surface.
void drawSurface(SDL_Surface* surface, const unsigned int xDest,
                 const unsigned int yDest, const unsigned int xSrc,
                 const unsigned int ySrc, const unsigned int wSrc, 
                 const unsigned int hSrc) {
	assert(surface != NULL);
	SDL_Rect dest;
	SDL_Rect source;
	dest.x = xDest;
	dest.y = yDest;
	source.x = xSrc;
	source.y = ySrc;
	source.w = wSrc;
	source.h = hSrc;
	if (SDL_BlitSurface(surface, &source, screen, &dest) < 0) {
		std::cout << "Surface blit failed:" << SDL_GetError() << std::endl;
		exit(1);
	}
} 
     
// Draws an entire SDL surface onto the screen surface
void drawSurface(SDL_Surface* surface, const unsigned int xDest,
                 const unsigned int yDest) {
	assert(surface != NULL);
	SDL_Rect dest;
	dest.x = xDest;
	dest.y = yDest;
	if (SDL_BlitSurface(surface, NULL, screen, &dest) < 0) {
		std::cout << "surface blit failed:" << SDL_GetError() << std::endl;
		exit(1);
	}
} 

// Draws an entire SDL surface onto the screen surface with an alpha component
void drawSurface(SDL_Surface* surface, const unsigned int xDest,
                 const unsigned int yDest, const unsigned char alpha) {
	assert(surface != NULL);
	SDL_Rect dest;
	dest.x = xDest;
	dest.y = yDest;
	if (SDL_SetAlpha(surface, SDL_SRCALPHA, alpha) < 0) {
		std::cout << "Set alpha failed:" << SDL_GetError() << std::endl;
		exit(1);
	}
	if (SDL_BlitSurface(surface, NULL, screen, &dest) < 0) {
		std::cout << "Surface blit failed:" << SDL_GetError() << std::endl;
		exit(1);
	}
}

void drawToTile(SDL_Surface* surface, const unsigned int x,
                const unsigned int y) {
	if ((x >= windowX) && (x < windowX + windowW) && (y >= windowY) &&
	    (y < windowY + windowH)) {
		drawSurface(surface, (x - windowX) * SPRITE_WIDTH,
					(y - windowY) * SPRITE_HEIGHT);
	}
}

void drawToTile(SDL_Surface* surface, const unsigned int x,
                const unsigned int y, const unsigned char alpha) {
	if ((x >= windowX) && (x < windowX + windowW) && (y >= windowY) &&
	    (y < windowY + windowH)) {
		drawSurface(surface, (x - windowX) * SPRITE_WIDTH,
					(y - windowY) * SPRITE_HEIGHT, alpha);
	}
}

// Loads a bitmap image from a file into an SDL surface
SDL_Surface* loadBitmap(const char* fn) {
	SDL_Surface *surface;
	SDL_Surface *temp;
	//std::cout << "Loading bitmap image: " << fn << " ... ";
	temp = SDL_LoadBMP(fn);
	if (temp == NULL) {
		std::cout << "Loading bitmap image: " << fn << " failed: "
			   	  << SDL_GetError() << std::endl;
		exit(1);
	}
	//std::cout<<"[ OK ]\n";
	SDL_SetColorKey(temp, SDL_SRCCOLORKEY, SDL_MapRGB(temp->format, 0, 255, 0));
	surface = SDL_DisplayFormat(temp);
	SDL_FreeSurface(temp);
	return surface;
}

// I believe this came from the SDL manual/tutorials.
// It gets the colour of a pixel from a surface.
Uint32 getPixel(SDL_Surface* surface, const unsigned int x,
                const unsigned int y) {
	assert (surface != NULL);
	unsigned int bpp = surface->format->BytesPerPixel;
	// Here p is the address to the pixel we want to retrieve
	Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
	switch (bpp) {
		case 1:
			return *p;
		case 2:
			return *(Uint16*)p;
		case 3:
			if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
				return p[0] << 16 | p[1] << 8 | p[2];
			} else {
				return p[0] | p[1] << 8 | p[2] << 16;
			}
		case 4:
			return *(Uint32*)p;
		default:
			return 0; // shouldn't happen, but avoids warnings
	}
}

// I believe this came from the SDL manual/tutorials.
// It sets the colour of a pixel on a surface.
void putPixel(SDL_Surface* surface, const unsigned int x, const unsigned int y,
              Uint32 pixel) {
	assert (surface != NULL);
	unsigned int bpp = surface->format->BytesPerPixel;
	// Here p is the address to the pixel we want to set
	Uint8 *p = (Uint8*)surface->pixels + y * surface->pitch + x * bpp;
	switch (bpp) {
		case 1:
			*p = pixel;
			break;
 		case 2:
			*(Uint16*)p = pixel;
			break;
		case 3:
			if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
				p[0] = (pixel >> 16) & 0xff;
				p[1] = (pixel >> 8) & 0xff;
				p[2] = pixel & 0xff;
			} else {
				p[0] = pixel & 0xff;
				p[1] = (pixel >> 8) & 0xff;
				p[2] = (pixel >> 16) & 0xff;
			}
			break;
		case 4:
			*(Uint32 *)p = pixel;
			break;
	}
}

// This will use SDL to rapidly fill a rectangle in a surface
// with a certain colour.
void fillRect(SDL_Surface* surface, const unsigned int xDest,
              const unsigned int yDest, const unsigned int wDest,
              const unsigned int hDest, const unsigned int colour) {
	assert(surface != NULL);
	SDL_Rect r;
	r.x = xDest;
	r.y = yDest;
	r.w = wDest;
	r.h = hDest;
	if (SDL_FillRect(surface, &r, colour) < 0) {
		std::cout << "WARNING: SDL_FillRect() failed: " << SDL_GetError();
	}
}

// This function will tint a surface by an RGB modifier.
// I am using hardware surfaces, which will probably make this function slow.
// It should therefore be used carefuly.
SDL_Surface* tint(SDL_Surface* surface, const signed char rMod,
                  const signed char gMod, const signed char bMod) {
	assert(surface != NULL);
	unsigned int tempPixel;
	unsigned char r, g, b;
	sLock(surface);
	// SDL_LockSurface(surface);
	for (signed int y = 0; y < surface->h; ++y) {
		for (signed int x = 0;x < surface->w; ++x) {
			tempPixel = getPixel(surface, x, y);
			SDL_GetRGB(tempPixel, surface->format, &r, &g, &b);
			// If the pixel is not part of the mask key (00FF00)
			if (! ((r == 0) && (g == 255) && (b == 0))) {
				if (! ((r == 0) && (g == 0) && (b == 0))) {
					if ((r >= (255 - rMod)) && (rMod > 0)) {
						r = 255;
					} else if ((r < (rMod * -1)) && (rMod < 0)) {
						r = 0;
					} else {
						r += rMod;
					}
					if ((g >= (255 - gMod)) && (gMod > 0)) {
						g = 255;
					} else if ((g < (gMod * -1)) && (gMod < 0)) {
						g = 0;
					} else {
						g += gMod;
					}
					if ((b >= (255 - bMod)) && (bMod > 0)) {
						b = 255;
					} else if ((b < (bMod * -1)) && (bMod < 0)) {
						b = 0;
					} else {
						b += bMod;
					}
				}
			}
			tempPixel = SDL_MapRGB(surface->format, r, g, b);
			putPixel(surface, x, y, tempPixel);
		}
	}
	sUlock(surface);
	return surface;
}

// This function will tint the screen by an rgb modifier and alpha
// I may make it possible to stack tints..
// it could be costly for the hardware though.
void tintScreen(const unsigned char r, const unsigned char g,
                const unsigned char b, const unsigned char a) {
	SDL_Rect rect;
	screenFilterAlpha = a;
	if (screenFilter != NULL) {
			SDL_FreeSurface(screenFilter);
	}
	screenFilter = SDL_CreateRGBSurface(SDL_SWSURFACE|SDL_SRCALPHA, 
	               xRes, yRes, bpp, 0, 0, 0, 0);
	rect.x = 0;
	rect.y = 0;
	rect.w = xRes;
	rect.h = yRes;
	SDL_FillRect(screenFilter, &rect,
	             SDL_MapRGB(screenFilter->format, r, g, b));
}

void untintScreen() {
	assert(screenFilter != NULL);
	SDL_FreeSurface(screenFilter);
	screenFilter = NULL;
}

// This function will tint a tile red. It will be extended in the future.
// Unlike the other tint functions, this is not persistent, and will need to be
// called every frame.
// Ask me if you want me to make it persistent. It'll require a new data
// structure though ;D
void tintTile(const unsigned int x, const unsigned int y) {
	if (tileFilter == NULL) {
		SDL_Rect rect;
		tileFilter = SDL_CreateRGBSurface(SDL_SWSURFACE|SDL_SRCALPHA,
		                                  SPRITE_WIDTH, SPRITE_HEIGHT,
		                                  bpp, 0, 0, 0, 0);
		rect.x = 0;
		rect.y = 0;
		rect.w = SPRITE_WIDTH;
		rect.h = SPRITE_HEIGHT;
		SDL_FillRect(tileFilter, &rect,
		             SDL_MapRGB(tileFilter->format, 255, 0, 0));
	}
	drawToTile(tileFilter, x, y, 128);
}

#ifdef WINDOWS
// Some wierd windows code which gets the console working.
// I stole it from somewhere.
static void activateConsole() {
	AllocConsole();
    
	HANDLE newConsoleInput = GetStdHandle(STD_INPUT_HANDLE);
	HANDLE newConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    
	int inFd = _open_osfhandle((long)newConsoleInput, _O_TEXT);
	int outFd = _open_osfhandle((long)newConsoleOutput, _O_TEXT);
    
	FILE* consoleIn = _fdopen(inFd, "r");
	FILE* consoleOut = _fdopen(outFd, "w");
    
	setvbuf(consoleIn, NULL, _IONBF, 0);
	setvbuf(consoleOut, NULL, _IONBF, 0);
    
	*stdin = *consoleIn;
	*stdout = *consoleOut;
}
#endif /* WINDOWS */
